<# .SYNOPSIS Configure Account Lockout Policies using positional arguments - Secpol Policy Framework. .SCRIPTTYPE Computer Configuration .DESCRIPTION This script applies Account Lockout Policy settings using positional arguments based on the PolicyDatabase array. Arguments are direct values for security policy settings related to account lockout behavior. ACCOUNT LOCKOUT POLICIES (4 policies): 1. Account lockout duration - How long accounts remain locked (0-99999 minutes) 2. Account lockout threshold - Failed logon attempts before lockout (0-999) 3. Allow Administrator account lockout - Whether admin accounts can be locked (0 or 1) 4. Reset account lockout counter after - Time before counter resets (0-99999 minutes) To use this script: 1. The PolicyDatabase array contains the specific account lockout policies 2. Each policy object needs: Name, KeyGroup ([System Access]), Key 3. Run with appropriate arguments for each policy in the database Examples: .\Set-AccountPolicies.ps1 '["30","5","0","30"]' This configures all 4 account lockout policies: - Lockout duration: 30 minutes - Lockout threshold: 5 attempts - Admin lockout: Disabled - Reset counter: 30 minutes .\Set-AccountPolicies.ps1 '["","5","","15"]' -WhatIf Preview mode - only configures threshold (5 attempts) and reset counter (15 minutes) .PARAMETER PolicyValues Positional string arguments corresponding to each account lockout policy in the PolicyDatabase. Values should be integers within the specified ranges for each policy. .PARAMETER LogLevel Set logging verbosity: Silent, Normal, Verbose, Debug .PARAMETER LogPath Path for log file output. If not specified, auto-configures to agent logs directory. .PARAMETER WhatIf Preview changes without applying them. .NOTES - Requires administrative privileges (Run as Administrator) - Uses Windows secedit.exe command internally for security policy configuration - Empty arguments or unspecified policies will be skipped - Invalid format arguments will display warnings and be skipped - Changes take effect immediately after secedit import - Comprehensive logging and error handling for production use ACCOUNT LOCKOUT POLICY DETAILS: - Account lockout duration: 0 = accounts locked until admin unlocks, >0 = auto-unlock after minutes - Account lockout threshold: 0 = never lock accounts, >0 = lock after failed attempts - Allow Administrator account lockout: 0 = admin exempt, 1 = admin can be locked - Reset account lockout counter after: Minutes before failed attempt counter resets #> param( [Parameter(Position=0, ValueFromRemainingArguments=$true)] [string[]]$PolicyValuesArray = @("[]"), [ValidateSet('Silent','Normal','Verbose','Debug')] [string]$LogLevel = 'Normal', [string]$LogPath = $null, [switch]$WhatIf ) # Combine all arguments into a single PolicyValues string # First, try to get the original command line with proper quotes $PolicyValues = $null try { $currentPID = $PID Write-Host "Current Process ID: $currentPID" -ForegroundColor Cyan $process = Get-CimInstance Win32_Process -Filter "ProcessId = $currentPID" if ($process) { $commandLine = $process.CommandLine Write-Host "Full command line: $commandLine" -ForegroundColor Yellow if ($commandLine) { # Get the script name for more precise regex matching $scriptName = [System.IO.Path]::GetFileName($MyInvocation.MyCommand.Path) $escapedScriptName = [regex]::Escape($scriptName) # Extract the first argument after this specific script (with all quotes intact) # Stop at known parameters: -LogLevel, -LogPath, -WhatIf, or end of string $pattern = "-File\s+`"[^`"]*\\$escapedScriptName`"\s+(.+?)(?:\s+(?:-LogLevel|-LogPath|-WhatIf)|$)" Write-Host "Using regex pattern: $pattern" -ForegroundColor DarkGray if ($commandLine -match $pattern) { $rawArgument = $matches[1].Trim() Write-Host "Raw argument extracted: $rawArgument" -ForegroundColor Magenta # Remove outer quotes if present if ($rawArgument -match '^"(.*)"$') { $PolicyValues = $matches[1] } else { $PolicyValues = $rawArgument } Write-Host "Extracted PolicyValues from command line: $PolicyValues" -ForegroundColor Green } else { Write-Host "Command line regex did not match. Command line: $commandLine" -ForegroundColor Red } } else { Write-Host "CommandLine property is null or empty" -ForegroundColor Red } } else { Write-Host "Failed to get process information for PID $currentPID" -ForegroundColor Red } } catch { Write-Host "Error extracting from command line: $($_.Exception.Message)" -ForegroundColor Red Write-Verbose "Could not extract from command line: $($_.Exception.Message)" } # Fallback: Use parameter-based approach if command line extraction failed if (-not $PolicyValues) { $PolicyValues = if ($PolicyValuesArray.Count -gt 1) { # Multiple arguments - join them back together $PolicyValuesArray -join '' } else { # Single argument - use as-is $PolicyValuesArray[0] } Write-Verbose "Using parameter-based PolicyValues: $PolicyValues" } # Account Lockout Policy Database - All 4 account lockout policies $PolicyDatabase = @( @{ Name = "Account lockout duration"; KeyGroup = "[System Access]"; Key = "LockoutDuration" }, @{ Name = "Account lockout threshold"; KeyGroup = "[System Access]"; Key = "LockoutBadCount" }, @{ Name = "Allow Administrator account lockout"; KeyGroup = "[System Access]"; Key = "AllowAdministratorLockout" }, @{ Name = "Reset account lockout counter after"; KeyGroup = "[System Access]"; Key = "ResetLockoutCount" } ) # Script-wide variables $script:LogFile = $null $script:StartTime = Get-Date $script:ProcessedCount = 0 $script:SuccessCount = 0 $script:FailureCount = 0 $script:SkippedCount = 0 # Initialize logging function Initialize-LogPath { if ($LogPath) { $logDir = Split-Path $LogPath -Parent if ($logDir -and -not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } return $LogPath } # Try to get agent directory, fallback to script directory $baseDir = $PSScriptRoot try { $registryPath = if ([Environment]::Is64BitOperatingSystem) { "HKLM:\SOFTWARE\WOW6432Node\AdventNet\DesktopCentral\DCAgent" } else { "HKLM:\SOFTWARE\AdventNet\DesktopCentral\DCAgent" } $agentDir = Get-ItemProperty -Path $registryPath -Name "DCAgentInstallDir" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty DCAgentInstallDir if ($agentDir -and (Test-Path $agentDir)) { $baseDir = $agentDir } } catch { Write-Verbose "Using script directory for logs" } # Create log directory and file path $auditDir = Join-Path (Join-Path $baseDir "logs") "SecurityPolicies" if (-not (Test-Path $auditDir)) { New-Item -ItemType Directory -Path $auditDir -Force | Out-Null } $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $scriptName = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.ScriptName) if ([string]::IsNullOrEmpty($scriptName)) { $scriptName = "Set-AccountPolicies1" } return Join-Path $auditDir "${scriptName}_$timestamp.log" } $script:LogFile = try { Initialize-LogPath } catch { $null } # Logging Functions function Write-Log { param( [Parameter(Mandatory=$true)] [string]$Message, [ValidateSet('Info','Warning','Error','Debug','Success')] [string]$Level = 'Info', [string]$Component = 'AccountPolicy' ) $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $logMessage = "[$timestamp] [$Level] [$Component] $Message" # Console output based on level and LogLevel setting switch ($Level) { 'Error' { if ($LogLevel -ne 'Silent') { Write-Error $Message } } 'Warning' { if ($LogLevel -notin @('Silent')) { Write-Warning $Message } } 'Success' { if ($LogLevel -notin @('Silent')) { Write-Host $Message -ForegroundColor Green } } 'Debug' { if ($LogLevel -eq 'Debug') { Write-Host $Message -ForegroundColor Gray } } 'Info' { if ($LogLevel -notin @('Silent')) { Write-Host $Message } } } # File output if LogPath is specified if ($script:LogFile) { Add-Content -Path $script:LogFile -Value $logMessage -Encoding UTF8 } } function Write-ProgressLog { param( [int]$Current, [int]$Total, [string]$Activity = "Processing Account Lockout Policies", [string]$CurrentItem = "" ) if ($LogLevel -ne 'Silent') { $percentComplete = if ($Total -gt 0) { ($Current / $Total) * 100 } else { 0 } Write-Progress -Activity $Activity -Status "Processing $Current of $Total - $CurrentItem" -PercentComplete $percentComplete } } function Test-Admin { $id = [Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object Security.Principal.WindowsPrincipal($id) return $p.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } function Initialize-Script { Write-Log "========== Account Lockout Policy Framework Script Started ==========" Write-Log "Script: $($MyInvocation.MyCommand.Name)" Write-Log "User: $env:USERNAME" Write-Log "Computer: $env:COMPUTERNAME" Write-Log "PowerShell Version: $($PSVersionTable.PSVersion)" Write-Log "Log Level: $LogLevel" if ($script:LogFile) { Write-Log "Log File: $($script:LogFile)" } else { Write-Log "Logging: Console only" } if ($WhatIf) { Write-Log "WhatIf mode enabled - no changes will be applied" -Level Warning } if (-not (Test-Admin)) { Write-Log "Administrator privileges required. Please run this script as Administrator." -Level Error throw "Administrator privileges required" } Write-Log "Administrator check passed" -Level Success if (-not $PolicyDatabase -or $PolicyDatabase.Count -eq 0) { Write-Log "PolicyDatabase is empty. Please configure policies before running." -Level Warning return $false } # Validate required properties for each policy foreach ($policy in $PolicyDatabase) { $hasName = $policy.ContainsKey('Name') -or ($policy.PSObject.Properties.Name -contains 'Name') $hasKeyGroup = $policy.ContainsKey('KeyGroup') -or ($policy.PSObject.Properties.Name -contains 'KeyGroup') $hasKey = $policy.ContainsKey('Key') -or ($policy.PSObject.Properties.Name -contains 'Key') if (-not ($hasName -and $hasKeyGroup -and $hasKey)) { $missingProps = @() if (-not $hasName) { $missingProps += 'Name' } if (-not $hasKeyGroup) { $missingProps += 'KeyGroup' } if (-not $hasKey) { $missingProps += 'Key' } Write-Log "Policy '$($policy.Name)' missing required properties: $($missingProps -join ', ')" -Level Error return $false } } Write-Log "PolicyDatabase validation passed - contains $($PolicyDatabase.Count) Account Lockout policies" -Level Success return $true } if (-not (Initialize-Script)) { return } # Initialize secedit export and import functions $exportPath = Join-Path -Path $PSScriptRoot -ChildPath "secpol_export.inf" $modifiedPath = Join-Path -Path $PSScriptRoot -ChildPath "secpol_modified.inf" Write-Log "Exporting current security policy to: $exportPath" -Level Debug secedit /export /cfg $exportPath | Out-Null function Import-InfFile { param ([string]$filePath) $data = @{} $currentSection = "" foreach ($line in Get-Content $filePath) { $line = $line.Trim() if ($line -match "^\[(.+)\]$") { $currentSection = $matches[1] if (-not $data.ContainsKey($currentSection)) { $data[$currentSection] = @{} } } elseif ($line -and -not $line.StartsWith(";") -and $currentSection) { $splitIndex = $line.IndexOf("=") if ($splitIndex -gt 0) { $key = $line.Substring(0, $splitIndex).Trim() $value = $line.Substring($splitIndex + 1).Trim() $data[$currentSection][$key] = $value } } } return $data } $policyData = Import-InfFile -filePath $exportPath function Write-InfFile { param ( [hashtable]$policyData, [string]$filePath ) $content = @() if ($policyData.ContainsKey('Unicode')) { $content += "[Unicode]" foreach ($key in $policyData['Unicode'].Keys) { $content += "$key = $($policyData['Unicode'][$key])" } $content += "" } foreach ($section in $policyData.Keys | Where-Object { $_ -ne 'Unicode' -and $_ -ne 'Version' }) { $content += "[$section]" foreach ($key in $policyData[$section].Keys) { $content += "$key = $($policyData[$section][$key])" } $content += "" } if ($policyData.ContainsKey('Version')) { $content += "[Version]" foreach ($key in $policyData['Version'].Keys) { $content += "$key = $($policyData['Version'][$key])" } $content += "" } $content | Out-File -FilePath $filePath -Encoding ASCII } function Set-SecpolRow { param( [Parameter(Mandatory=$true)] [string]$Name, [Parameter(Mandatory=$true)] [string]$KeyGroup, [Parameter(Mandatory=$true)] [string]$Key, [Parameter(Mandatory=$true)] [string]$Value ) $section = ($KeyGroup.Trim() -replace '^\[|\]$').Trim() $keyName = $Key.Trim() $cleanValue = ($Value -replace '[\u200B-\u200D\uFEFF]', '').Trim() if ([string]::IsNullOrEmpty($cleanValue)) { Write-Log "Policy '${Name}': Value is empty or whitespace. Skipped." -Level Warning return $false } # Validate integer values for account lockout policies if ($keyName -in @('LockoutDuration', 'LockoutBadCount', 'AllowAdministratorLockout', 'ResetLockoutCount')) { try { $intValue = [int]$cleanValue if ($keyName -eq 'AllowAdministratorLockout' -and $intValue -notin @(0, 1)) { Write-Log "Invalid value for ${keyName}: $cleanValue. Must be 0 or 1." -Level Error return $false } if ($keyName -in @('LockoutDuration', 'ResetLockoutCount') -and ($intValue -lt 0 -or $intValue -gt 99999)) { Write-Log "Invalid value for ${keyName}: $cleanValue. Must be between 0 and 99999." -Level Error return $false } if ($keyName -eq 'LockoutBadCount' -and ($intValue -lt 0 -or $intValue -gt 999)) { Write-Log "Invalid value for ${keyName}: $cleanValue. Must be between 0 and 999." -Level Error return $false } } catch { Write-Log "Invalid integer value for ${keyName}: $cleanValue" -Level Error return $false } } if (-not $policyData.ContainsKey($section)) { Write-Log "Creating new section: [$section]" -Level Info $policyData[$section] = @{} } Write-Log "Setting [$section] $keyName = $cleanValue for policy: $Name" -Level Info $policyData[$section][$keyName] = $cleanValue return $true } # Parse PolicyValues array string to array function Parse-PolicyValuesArray { param([string]$ArrayString) # Check if input is JSON format (starts with [ and ends with ]) if ($ArrayString -match '^\s*\[.*\]\s*$') { Write-Log "Detected JSON format input, attempting to parse..." -Level Debug try { # Parse the JSON array string $Arguments = ConvertFrom-Json $ArrayString Write-Log "Successfully parsed JSON policy values array: $($Arguments.Count) values provided" -Level Info return $Arguments } catch { Write-Log "Failed to parse as JSON: $($_.Exception.Message)" -Level Warning Write-Log "Falling back to positional argument parsing..." -Level Info } } else { Write-Log "Input is not in JSON format (doesn't start with [ ), using as positional arguments" -Level Info } # Fallback: Use PolicyValuesArray as positional arguments Write-Log "Using $($PolicyValuesArray.Count) positional arguments" -Level Info return $PolicyValuesArray } Write-Log "Arguments provided: $PolicyValues" # Parse PolicyValues array string to get individual arguments $Arguments = Parse-PolicyValuesArray -ArrayString $PolicyValues function Save-SecpolChanges { try { Write-InfFile -policyData $policyData -filePath $modifiedPath Write-Log "Updated INF file saved to $modifiedPath" -Level Success if (-not $WhatIf) { Write-Log "Importing security policy changes..." -Level Info secedit /configure /db secedit.sdb /cfg $modifiedPath /areas SECURITYPOLICY | Out-Null Write-Log "Security policy updated successfully from Account Lockout Policy input." -Level Success } else { Write-Log "WHATIF: Would import security policy from $modifiedPath" -Level Info } } catch { Write-Log "Failed to update security policy: $($_.Exception.Message)" -Level Error throw } } # Main processing loop with comprehensive logging Write-Log "Starting Account Lockout policy processing for $($PolicyDatabase.Count) policies" Write-Log "Arguments provided: $($Arguments.Count)" for ($i = 0; $i -lt $PolicyDatabase.Count; $i++) { $script:ProcessedCount++ $policy = $PolicyDatabase[$i] Write-ProgressLog -Current ($i + 1) -Total $PolicyDatabase.Count -CurrentItem $policy.Name if ($i -ge $Arguments.Count) { Write-Log "Policy #$($i+1): '$($policy.Name)' - Not Configured (no argument provided)" -Level Info $script:SkippedCount++ continue } $arg = ([string]$Arguments[$i]).Trim() if ([string]::IsNullOrEmpty($arg)) { Write-Log "Policy #$($i+1): '$($policy.Name)' - Not Configured (empty argument)" -Level Info $script:SkippedCount++ continue } Write-Log "Processing policy #$($i+1): '$($policy.Name)' with argument: '$arg'" -Level Info # Validate policy structure using proper hashtable validation $hasName = $policy.ContainsKey('Name') $hasKeyGroup = $policy.ContainsKey('KeyGroup') $hasKey = $policy.ContainsKey('Key') if (-not ($hasName -and $hasKeyGroup -and $hasKey)) { $missingProps = @() if (-not $hasName) { $missingProps += 'Name' } if (-not $hasKeyGroup) { $missingProps += 'KeyGroup' } if (-not $hasKey) { $missingProps += 'Key' } Write-Log "Policy #$($i+1) missing required properties: $($missingProps -join ', '). Skipping..." -Level Error $script:FailureCount++ continue } # Show policy details in debug mode if ($LogLevel -eq 'Debug') { Write-Log "Policy Details - Name: $($policy.Name), KeyGroup: $($policy.KeyGroup), Key: $($policy.Key), Argument: $arg" -Level Debug } try { if (Set-SecpolRow -Name $policy.Name -KeyGroup $policy.KeyGroup -Key $policy.Key -Value $arg) { $script:SuccessCount++ Write-Log "Successfully processed Account Lockout policy: '$($policy.Name)'" -Level Success } else { $script:FailureCount++ Write-Log "Failed to process Account Lockout policy: '$($policy.Name)'" -Level Error } } catch { $script:FailureCount++ Write-Log "Exception processing Account Lockout policy '$($policy.Name)': $($_.Exception.Message)" -Level Error } Write-Log "--- Policy #$($i+1) completed ---" -Level Debug } # Commit any pending secpol changes Write-Log "Committing Account Lockout security policy changes..." -Level Info try { Save-SecpolChanges Write-Log "Account Lockout security policy changes committed successfully" -Level Success } catch { Write-Log "Failed to commit Account Lockout security policy changes: $($_.Exception.Message)" -Level Error $script:FailureCount++ } # Final summary and cleanup function Write-CompletionSummary { $endTime = Get-Date $duration = $endTime - $script:StartTime Write-Log "========== Account Lockout Policy Framework Execution Summary ==========" -Level Info Write-Log "Execution Duration: $($duration.ToString('hh\:mm\:ss'))" -Level Info Write-Log "Total Policies in Database: $($PolicyDatabase.Count)" -Level Info Write-Log "Successfully Applied: $script:SuccessCount" -Level Success Write-Log "Failed: $script:FailureCount" -Level $(if ($script:FailureCount -gt 0) { 'Warning' } else { 'Info' }) Write-Log "Not Configured: $script:SkippedCount" -Level Info $actuallyProcessed = $script:SuccessCount + $script:FailureCount if ($actuallyProcessed -gt 0) { $successRate = [math]::Round(($script:SuccessCount / $actuallyProcessed) * 100, 2) Write-Log "Success Rate: $successRate% (of actually processed policies)" -Level Info } if ($script:FailureCount -gt 0) { Write-Log "Some Account Lockout policies failed to apply. Check the log for details." -Level Warning Write-Log "Common issues: Invalid values, insufficient permissions, secedit errors" -Level Warning } if ($WhatIf) { Write-Log "WhatIf mode was enabled - no actual changes were made." -Level Info } if ($script:LogFile) { Write-Log "Detailed log saved to: $script:LogFile" -Level Info } Write-Log "========== Account Lockout Policy Framework Execution Complete ==========" -Level Info } Write-CompletionSummary # Return exit code based on results if ($script:FailureCount -gt 0) { exit 1 } else { exit 0 }